/*
 * Copyright (c) 2004 - 2012 Eike Stepper (Berlin, Germany) and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Eike Stepper - initial API and implementation
 */
package org.eclipse.emf.extensions;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.MessageFormat;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.emf.cdo.server.internal.db.CDODBSchema;
import org.eclipse.emf.db.Activator;
import org.eclipse.emf.db.util.DBException;
import org.eclipse.emf.db.util.DBModelInformationCache;
import org.eclipse.emf.db.util.DBUtil;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.ETypedElement;

import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

public abstract class DBSchema implements IDBSchema {
    private final String name;

    private List<IDBTable> tables;

    private boolean locked;

    private final Connection connection;

    private Set<String> reservedWords;

    private final EPackage pkg;

    protected DBSchema(Connection connection, EPackage pkg) throws SQLException {
        diagnose(pkg);
        this.connection=connection;
        this.pkg=pkg;
        name=connection.getCatalog();
    }

    //    @Override
    //    public IDBSchema getSchema() {
    //        return this;
    //    }

    @Override
    public String getName() {
        return name;
    }

    /**
     * @throws SQLException 
     * @since 4.0
     */
    @Override
    public void dropTable(IDBTable table) throws SQLException {
        assertUnlocked();
        Statement statement=null;
        try {
            statement=connection.createStatement();
            String sql=getDropTableSQL(table.getDescriptor().getName());
            statement.execute(sql);
            tables.remove(table);
        } finally {
            DBUtil.close(statement);
        }
    }

    protected String getDropTableSQL(String tableName) {
        return "DROP TABLE " + tableName; //$NON-NLS-1$
    }

    /**
     * @throws SQLException 
     * @since 2.0
     */
    @Override
    public IDBTable findTable(IDBTableDescriptor descriptor) throws SQLException {
        initTables();
        //        String tableName=clazz.getName();
        //        tableName=getSafeName(tableName);
        for (IDBTable table : Iterables.filter(tables, DBTable.class)) {
            if (descriptor.getName().equalsIgnoreCase(table.getDescriptor().getName()))
                return table;
        }
        return null;
    }

    //    @Override
    //    public IDBTable findTable(String name) throws SQLException {
    //        initTables();
    //        for (IDBTable table : Iterables.filter(tables, NativeTable.class)) {
    //            if (name.equalsIgnoreCase(table.getName()))
    //                return table;
    //        }
    //        return null;
    //    }

    /**
     * @throws SQLException 
     * @since 2.0
     */
    @Override
    public IDBTable[] getTables() throws SQLException {
        initTables();
        return tables.toArray(new DBTable[tables.size()]);
    }

    private void initTables() throws SQLException {
        if (tables == null) {
            tables=listTables();
        }
    }

    protected List<IDBTable> listTables() throws SQLException {
        Statement statement=null;
        ResultSet rSet=null;
        try {
            Map<String, EClass> map=Maps.newHashMap();
            for (EClass clazz : DBModelInformationCache.getConcreteClasses(pkg)) {
                map.put(getSafeName(clazz.getName()).toUpperCase(), clazz);
            }
            List<IDBTable> tables=Lists.newArrayList();
            statement=connection.createStatement();
            rSet=connection.getMetaData().getTables(null, "public", "%", null); //statement.executeQuery(getListTablesSQL());
            while (rSet.next()) {
                String name=rSet.getString(3);
                EClass clazz=map.get(name.toUpperCase());
                IDBTableDescriptor descriptor;
                if (clazz != null)
                    descriptor=new DBModelTableDescriptor(this, clazz);
                else
                    descriptor=new DBNativeTableDescriptor(name);

                tables.add(createTable(this, descriptor));
            }
            return tables;
        } finally {
            if (rSet != null)
                rSet.close();
            if (statement != null)
                statement.close();
        }
    }

    public abstract IDBTypeProvider getTypeProvider();

    public boolean isLocked() {
        return locked;
    }

    public boolean lock() {
        return locked=true;
    }

    @Override
    public IDBTable createTable(IDBTableDescriptor descriptor) throws SQLException {
        Statement statement=connection.createStatement();
        try {
            statement.execute(getCreateTableSQL(descriptor));
            IDBTable dbTable=createTable(this, descriptor);
            connection.commit();
            tables.add(dbTable);
            return dbTable;
        } finally {
            DBUtil.close(statement);
        }
    }

    protected abstract IDBTable createTable(DBSchema schema, IDBTableDescriptor descriptor);

    protected abstract String getCreateTableSQL(IDBTableDescriptor descriptor);

    public void assertUnlocked() throws DBException {
        if (locked) {
            throw new DBException("DBSchema locked: " + name); //$NON-NLS-1$
        }
    }

    private boolean isReservedWord(String word) {
        if (reservedWords == null) {
            reservedWords=new HashSet<String>();
            for (String reservedWord : getReservedWords()) {
                reservedWords.add(reservedWord.toUpperCase());
            }
        }

        word=word.toUpperCase();
        return reservedWords.contains(word);
    }

    //    @Override
    public String getSafeName(String name) {
        if (isReservedWord(name))
            return name + '0';

        return name;
    }

    protected abstract String[] getReservedWords();

    @Override
    public Connection getConnection() {
        return connection;
    }

    private void diagnose(EPackage pkg) {
        for (EClass clazz : DBModelInformationCache.getConcreteClasses(pkg)) {
            // Check upper bounds
            for (EStructuralFeature feature : clazz.getEAllStructuralFeatures()) {
                if (feature.getLowerBound() != 0 && feature.getLowerBound() != 1)
                    throw new UnsupportedOperationException("Lower bound *must* be equals to zero or 1 " + feature); //$NON-NLS-1$
                if (feature.getUpperBound() != 1 && feature.getUpperBound() != ETypedElement.UNBOUNDED_MULTIPLICITY)
                    throw new UnsupportedOperationException("Upper bound *must* be equals to 1 or 'EStructuralFeature.UNBOUNDED_MULTIPLICITY' " + feature); //$NON-NLS-1$

                // Additionnal checks on EReferences
                if (feature instanceof EReference) {
                    EReference ref=(EReference) feature;
                    // 0..* relation to abstract is not allowed
                    if (ref.getUpperBound() == ETypedElement.UNBOUNDED_MULTIPLICITY) {
                        if (ref.getEOpposite() == null || ref.getEOpposite().getUpperBound() == ETypedElement.UNBOUNDED_MULTIPLICITY)
                            throw new UnsupportedOperationException("Multiple relation must have a 0..1 EOpposite: " + toString(ref)); //$NON-NLS-1$

                        if (ref.getEType() instanceof EClass) {
                            EClass type=(EClass) ref.getEType();
                            if (type.isAbstract())
                                throw new UnsupportedOperationException("Multiple relation to abstract class is not supported for performance issues: " //$NON-NLS-1$
                                        + toString(ref));
                        }
                    }
                    // Check that 0..1 - 0..1 has at least one containment reference
                    if (ref.getUpperBound() == 1 && ref.getEOpposite() != null && ref.getEOpposite().getUpperBound() == 1) {
                        if (!ref.isContainment() && !ref.getEOpposite().isContainment()) {
                            throw new UnsupportedOperationException("0..1 - 0..1 relation must have at least one containment reference: " + toString(ref)); //$NON-NLS-1$
                        }
                    }
                    // Check the reference without caring about the result
                    DBModelInformationCache.hasInheritance(ref);
                } else if (feature instanceof EAttribute) {
                    if (feature.getUpperBound() == ETypedElement.UNBOUNDED_MULTIPLICITY)
                        throw new UnsupportedOperationException("Multiple relations are forbidden for attributes: " + toString(feature)); //$NON-NLS-1$
                }
            }
        }
    }

    @Override
    public void createOrUpdateDBStructure() throws SQLException {
        for (EClass clazz : DBModelInformationCache.getConcreteClasses(pkg)) {
            List<String> columnNames=Lists.newArrayList();
            DBModelTableDescriptor descriptor=new DBModelTableDescriptor(this, clazz);
            IDBTable table=findTable(descriptor);
            if (table == null) {
                table=createTable(descriptor);
            }

            // Create column for EStructuralFeatures
            for (EStructuralFeature feature : clazz.getEAllStructuralFeatures()) {
                if (feature.getUpperBound() == 1) {
                    DBModelFieldDescriptor fieldDescriptor=new DBModelFieldDescriptor(this, feature);
                    IDBField field=table.getField(fieldDescriptor);
                    if (field == null) {
                        table.addField(fieldDescriptor);
                    }
                }
            }
            columnNames.remove(CDODBSchema.ATTRIBUTES_ID);

            columnNames.remove(CDODBSchema.ATTRIBUTES_CREATED);
            columnNames.remove(CDODBSchema.ATTRIBUTES_CONTAINER);
            if (!columnNames.isEmpty()) {
                Activator.log(IStatus.WARNING,
                        MessageFormat.format("The column {1} in table {0} got unused", table.getDescriptor().getName(), columnNames), null); //$NON-NLS-1$
            }
            connection.commit();
        }
    }

    public static String toString(EStructuralFeature ref) {
        return ref.getEContainingClass().getName() + " -> " + ref.getName() + " -> " + ref.getEType().getName(); //$NON-NLS-1$ //$NON-NLS-2$
    }

    @Override
    public EPackage getPackage() {
        return pkg;
    }

    @Override
    public String getColumnName(EStructuralFeature feature) {
        if (feature instanceof EReference && ((EReference) feature).isContainer())
            return CDODBSchema.ATTRIBUTES_CONTAINER;

        String result=feature.getName();
        return getSafeName(result);
    }

    @Override
    public boolean checkModificationTime() {
        return true;
    }

    @Override
    public String getTableName(EClass clazz) {
        return getSafeName(clazz.getName());
    }
}
